﻿using System;
using System.Text;
using Netbits.Common.Collections;

namespace Netbits.Common.General.CommandLine
{
	/// <summary>
	/// Represent a command-line parser.
	/// </summary>
	public class Parser
	{
		#region Private Variables

		LockedKeyedCollection<string, IArgument> _Arguments;
		bool _IsParsed;

		#endregion	

		#region Constructor

		/// <summary>
		/// Create a new instance of this command-line parsers
		/// </summary>
		public Parser()
		{
			_Arguments = new LockedKeyedCollection<string, IArgument>( item => item.ShortName );
		}

		#endregion

		#region Public Functions

		/// <summary>
		/// Add new command-line argument to this parser
		/// </summary>
		/// <param name="Argument_">Argument to add</param>
		public void AddArgument( IArgument Argument_ )
		{
			if( _Arguments.Contains( Argument_.ShortName ) )
				throw new Exception( "argument with the same key exists!" );

			_Arguments.Add( Argument_ );
		}

		/// <summary>
		/// Find the specified argument
		/// </summary>
		/// <param name="ShortName_">Name of the argument name</param>
		/// <returns>The specified argument otherwise an exception</returns>
		public IArgument GetArgument( string ShortName_ )
		{
			if( !_Arguments.Contains( ShortName_ ) )
				throw new Exception( "commandline argument with specified key not found" );

			return _Arguments[ShortName_];
		}

		/// <summary>
		/// Check if the specified argument is set
		/// </summary>
		/// <param name="ShortName_">Name of the argument to check</param>
		/// <returns>true if the argument is set on the command-line otherwise false</returns>
		public bool IsArgumentSet( string ShortName_ )
		{
			return GetArgument( ShortName_ ).IsSet;
		}

		/// <summary>
		/// Get the value of the specified argument
		/// </summary>
		/// <typeparam name="TValue">Type of the value to return</typeparam>
		/// <param name="ShortName_">Name of the argument to get the value</param>
		/// <param name="DefaultValue_">Default-value if argument not found or no value is set</param>
		/// <returns>The value of the specified argument otherwise DefaultValue_</returns>
		public TValue GetArgumentValue<TValue>( string ShortName_, TValue DefaultValue_ )
		{
			try
			{
				return GetArgumentValue<TValue>( ShortName_ );
			}
			catch
			{
				return DefaultValue_;
			}
		}

		/// <summary>
		/// Get the value of the specified argument
		/// </summary>
		/// <typeparam name="TValue">Type of the value to return</typeparam>
		/// <param name="ShortName_">Name of the argument to get the value</param>
		/// <returns>The value of the specified argument otherwise an exception</returns>
		public TValue GetArgumentValue<TValue>( string ShortName_ )
		{
			IArgument ia = GetArgument( ShortName_ );
			ArgumentBase<TValue> ab = ia as ArgumentBase<TValue>;
			if( ab.IsSet )
				return ab.Value;

			if( ab.IsDefaultSet )
				return ab.DefaultValue;

			throw new Exception( "argument not set and has no default-value" );
		}

		/// <summary>
		/// Print the help information for all specified arguments
		/// </summary>
		public void PrintHelp()
		{
			Console.WriteLine( "{0} arguments", System.IO.Path.GetFileName( Environment.GetCommandLineArgs()[0] ) );
			Console.WriteLine( "Arguments:" );
			foreach( IArgument icla in _Arguments )
				Console.WriteLine( "{0} {1} -- {2}", icla.ShortName, icla.Required, icla.Description );
			Console.WriteLine( Environment.NewLine );
		}

		/// <summary>
		/// Parse the applications command-line
		/// </summary>
		public void Parse()
		{
			if( _IsParsed )
				return;

			string[] fullargs = Environment.GetCommandLineArgs();

			if( fullargs.Length == 1 )
				return;

			string[] args = new string[fullargs.Length - 1];
			int pos = 0;
			for( int i = 1; i < fullargs.Length; i++ )
				args[pos++] = fullargs[i];

			Parse( args );

			_IsParsed = true;
		}

		/// <summary>
		/// Parse the specified arguments.
		/// </summary>
		/// <param name="Arguments_">Arguments to parse.</param>
		public void Parse( string[] Arguments_ )
		{
			StringBuilder sb = new StringBuilder();
			foreach( string s in Arguments_ )
				sb.AppendFormat( "{0} ", s );

			_Parse( sb.ToString() );
		}

		#endregion

		#region Private Functions

		void _ParseToArgument( StringBuilder Key_, StringBuilder Value_ )
		{
			string key = Key_.ToString();
			Key_.Length = 0;
			string value = Value_.ToString();
			Value_.Length = 0;

			if( !_Arguments.Contains( key ) )
			{
				Console.WriteLine( "warning: argument {0} has no effect", key );
				return;
			}

			IArgument ia = _Arguments[key];
			try
			{
				ia.Parse( value );
			}
			catch( Exception Ex_ )
			{
				throw new InvalidCastException( string.Format( "argument {0} has invalid value {1}. {2}", key, value, Ex_.Message ) );
			}
		}

		void _Parse( string ArgumentsLine_ )
		{
			StringBuilder sbKey = new StringBuilder();
			StringBuilder sbValue = new StringBuilder();

			StringBuilder sb = sbKey;
			bool instring = false;
			char lastchar = char.MinValue;

			foreach( char c in ArgumentsLine_ )
			{
				switch( c )
				{
					// Argument split
					case ' ':
						if( instring )
							sb.Append( c );
						else
							_ParseToArgument( sbKey, sbValue );

						sbKey.Clear();
						sbValue.Clear();
						sb = sbKey;
						break;

					// Split command and value
					case '=':
						if( instring )
							sb.Append( c );
						else
							sb = sbValue;
						break;

					// String start and end
					case '"':
						if( instring )
						{
							if( lastchar == '\\' )
								sb.Append( c );
							else
								instring = !instring;
						}
						else
							instring = !instring;

						break;

					// save char in current builder
					default:
						sb.Append( c );
						break;
				}

				lastchar = c;
			}

			if( sbKey.Length > 0 )
				_ParseToArgument( sbKey, sbValue );

			foreach( IArgument icla in _Arguments )
			{
				if( icla.Required && !icla.IsSet )
					throw new InvalidOperationException( string.Format( "argument {0} not set!", icla.ShortName ) );
			}
		}

		#endregion
	}
}
